#
# This file is a part of the open-eBackup project.
# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
# If a copy of the MPL was not distributed with this file, You can obtain one at
# http://mozilla.org/MPL/2.0/.
#
# Copyright (c) [2024] Huawei Technologies Co.,Ltd.
#
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
#

import ast
import os.path
import platform

from common.exception.common_exception import ErrCodeException
from common.file_common import exec_lchown_dir_recursively, create_dir_recursive
from common.util.exec_utils import exec_mkdir_cmd
from oracle import logger
from oracle.common.common import exec_sql_cmd, log_func_time_cost, close_child
from oracle.common.constants import PexpectResult, ScriptExitCode, ErrorCode, Platform
from oracle.common.sqlplus_func import get_omf_dest, \
    exec_select_cmd_with_child
from oracle.common.user_env_common import get_asm_user_env_by_linux
from oracle.common.windows_common import get_grid_home_path
from oracle.schemas.oracle_schemas import PDBRestoreInfo
from oracle.services.restore.restore_common import login_oracle_database_restore


class PdbRestore:
    def __init__(self, pid, pdb_list, is_overwrite, params, assist_ins_sid=None):
        self._pid = pid
        self.assist_ins_id = assist_ins_sid
        self.target_ins_id = ""
        self.main_backup_path = ""
        self.pdb_list = ast.literal_eval(pdb_list)
        self.ora_db_user = ""
        self.is_open_pdb = True
        self.pdb_restore_map = dict()
        self.cache_repo_path = ""
        self.is_overwrite = True if is_overwrite.lower() == 'true' else False
        self.restore_path = ""
        self.list_cluster_other = []
        self.target_omf_dest = ''
        self.target_db_name = ''
        self.target_child = None
        self.assist_child = None
        self.init_params(params)

    def init_params(self, params):
        self.target_ins_id = params['target_ins_id']
        self.ora_db_user = params['ora_db_user']
        self.oracle_group = params['oracle_group']
        self.main_backup_path = params['main_backup_path']
        self.cache_repo_path = params['cacheRepoPath']
        self.restore_path = params['restore_path']
        self.list_cluster_other = params['list_cluster_other']
        if params['is_open_pdb'] == "":
            self.is_open_pdb = True
        else:
            self.is_open_pdb = True if params['is_open_pdb'].lower() == 'true' else False
        self.assist_ins_login_params = {
            "pid": self._pid,
            "db_install_user": self.ora_db_user,
            "instance_name": self.assist_ins_id,
            'auth_pwd': f"job_copies_0_protectObject_auth_authPwd_{self._pid}",
            'auth_key': f"job_copies_0_protectObject_auth_authKey_{self._pid}"
        }
        self.target_ins_login_params = {
            "pid": self._pid,
            "db_install_user": self.ora_db_user,
            "instance_name": self.target_ins_id,
            'auth_pwd': f"job_targetObject_auth_authPwd_{self._pid}",
            'auth_key': f"job_targetObject_auth_authKey_{self._pid}"
        }
        self.oracle_home = params['oracle_home']
        self.asm_instance = params['asm_instance']
        self.asm_install_user = params['asm_install_user']

    def build_login_child(self):
        result, target_child = login_oracle_database_restore(
            self._pid, self.target_ins_id, self.target_ins_login_params, self.ora_db_user)
        if not result:
            raise ErrCodeException(ScriptExitCode.ERROR_ORACLE_EXESQL_FAILED,
                                   message="login target inst error,auth error")
        self.target_child = target_child

    def build_assist_child(self):
        result, assist_child = login_oracle_database_restore(
            self._pid, self.assist_ins_id, self.assist_ins_login_params, self.ora_db_user)
        if not result:
            raise ErrCodeException(ScriptExitCode.ERROR_ORACLE_EXESQL_FAILED,
                                   message="login assist inst error,auth error")
        self.assist_child = assist_child

    def exit(self):
        if self.target_child:
            close_child(self.target_child)
        if self.assist_child:
            close_child(self.assist_child)

    @log_func_time_cost
    def pdb_restore(self):
        try:
            self.build_login_child()
            self.build_assist_child()
            sql = "select db_unique_name from v$database;"
            result = exec_select_cmd_with_child(self.target_child, sql,
                                                parse_func=lambda item: item.get('DB_UNIQUE_NAME'))
            self.target_db_name = result[0]
            # 查询PDB的DBF文件
            self.query_all_pdb_dbf()
            logger.info(f"pdb_map:{self.pdb_restore_map}")
            for pdb_name in self.pdb_list:
                self.unplug_single_pdb(pdb_name)
            self.drop_single_target_pdbs()
            for pdb_name in self.pdb_list:
                # 针对每个pdb，执行检查操作
                self.check_single_pdb_pluggable(pdb_name)
            for pdb_name in self.pdb_list:
                if not self.handle_recover_path(pdb_name):
                    raise ErrCodeException(ErrorCode.INTERNAL_ERROR, message="create pdb name path error")
            logger.info(f"target omf:{self.target_omf_dest}")
            # 插入目标PDB，指定dbf位置
            for pdb_name in self.pdb_list:
                self.plug_single_pdb_into_target_ins(pdb_name)
                self.alter_single_pdb_open(pdb_name)
            logger.info("plugin into target ins success")
            return ScriptExitCode.SUCCESS
        except Exception as error:
            logger.exception(error)
            logger.error(f"pdb cluster post job error,{self._pid}, error:{error}")
            return ScriptExitCode.ERROR_ORACLE_EXESQL_FAILED
        finally:
            self.exit()

    @log_func_time_cost
    def pdb_cluster_pre_job(self):
        try:
            self.build_login_child()
            logger.info(f"pdb cluster pre job is start,{self._pid}")
            pdb_name_str = "(" + ",".join(f"'{pdb_name}'" for pdb_name in self.pdb_list) + ")"
            sql = f"select NAME from v$pdbs where OPEN_MODE = 'READ WRITE' and name in {pdb_name_str};"
            pdb_name_list = exec_select_cmd_with_child(self.target_child, sql, parse_func=lambda item: item.get('NAME'))
            if not pdb_name_list:
                logger.info(f"target pdb {pdb_name_str} not read write,{self._pid}")
                return ScriptExitCode.SUCCESS
            for pdb_name in pdb_name_list:
                sql = f"alter pluggable database {pdb_name} close immediate;"
                return_code, std_out = exec_sql_cmd(self.target_child, sql, PexpectResult.CLOSE_PDB)
                if not return_code:
                    logger.error(f'alter pluggable error, return_code: {return_code}. sql: {sql}, stdout:{std_out}')
                    raise ErrCodeException(ErrorCode.ORACLE_EXEC_SQL_FAILED, [std_out],
                                           message=f'close pdb {pdb_name} failed')
            logger.info(f"pdb cluster pre job success")
            return ScriptExitCode.SUCCESS
        except Exception as error:
            logger.error(f"pdb cluster post job error,{self._pid}, error:{error}")
            return ScriptExitCode.ERROR_ORACLE_EXESQL_FAILED
        finally:
            self.exit()

    @log_func_time_cost
    def pdb_cluster_post_job(self):
        try:
            self.build_login_child()
            logger.info(f"pdb cluster post job is start,{self._pid}")
            pdb_name_str = "(" + ",".join(f"'{pdb_name}'" for pdb_name in self.pdb_list) + ")"
            sql = f"select NAME, OPEN_MODE from v$pdbs where OPEN_MODE = 'MOUNTED' and name in {pdb_name_str};"
            pdb_name_list = exec_select_cmd_with_child(self.target_child, sql, parse_func=lambda item: item.get('NAME'))
            if not pdb_name_list:
                logger.info(f"no mounted pdb,{self._pid}")
                return ScriptExitCode.SUCCESS
            # 检查恢复任务选中的PDB集中，是否存在需要开启的PDB
            for pdb_name in pdb_name_list:
                sql = f"alter pluggable database {pdb_name} open;"
                return_code, std_out = exec_sql_cmd(self.target_child, sql, PexpectResult.CLOSE_PDB)
                if not return_code:
                    logger.error(f'alter pluggable error, return_code: {return_code}. sql: {sql}, stdout:{std_out}')
                    raise ErrCodeException(ErrorCode.ORACLE_EXEC_SQL_FAILED, [std_out],
                                           message=f'open pdb {pdb_name} failed')
            logger.info(f"pdb cluster post job success")
            return ScriptExitCode.SUCCESS
        except Exception as error:
            logger.error(f"pdb cluster post job error,{self._pid}, error:{error}")
            return ScriptExitCode.ERROR_ORACLE_EXESQL_FAILED
        finally:
            self.exit()

    def get_pdb_datafile_name(self, child, pdb_name):
        logger.info(f"get_pdb_datafile_name,{pdb_name},{self._pid}")
        cmd = f"select name from v$datafile where con_id = (select con_id from v$containers where name='{pdb_name}');"
        return exec_select_cmd_with_child(child, cmd, parse_func=lambda item: item.get('NAME'))

    def get_pdb_tempfile_name(self, child, pdb_name):
        logger.info(f"get_pdb_tempfile_name,{pdb_name},{self._pid}")
        cmd = f"select name from v$tempfile WHERE CON_ID = (SELECT CON_ID FROM V$CONTAINERS WHERE NAME='{pdb_name}');"
        return exec_select_cmd_with_child(child, cmd, parse_func=lambda item: item.get('NAME'))

    def get_pdb_info(self, child, pdb_name):
        logger.info(f"get_pdb_info,{pdb_name},{self._pid}")
        cmd = f"select name, guid from v$pdbs where name = '{pdb_name}';"
        result = exec_select_cmd_with_child(child, cmd)
        if not result:
            return "", ""
        return result[0].get("GUID"), result[0].get("NAME")

    @log_func_time_cost
    def query_all_pdb_dbf(self):
        for pdb_name in self.pdb_list:
            datafile_path_list = self.get_pdb_datafile_name(self.assist_child, pdb_name)
            tempfile_path_list = self.get_pdb_tempfile_name(self.assist_child, pdb_name)
            guid, _ = self.get_pdb_info(self.assist_child, pdb_name)
            pdb_restore_info = PDBRestoreInfo(pdb_name=pdb_name, guid=guid, datafile_list=datafile_path_list,
                                              tempfile_list=tempfile_path_list)
            logger.info(f"pdb_restore_info:{pdb_restore_info.dict()}")
            self.pdb_restore_map[pdb_name] = pdb_restore_info

    @log_func_time_cost
    def drop_single_target_pdbs(self):
        pdb_name_str = "(" + ",".join(f"'{pdb_name}'" for pdb_name in self.pdb_list) + ")"
        sql = f"select NAME,OPEN_MODE from v$pdbs where name in {pdb_name_str};"
        pdb_list = exec_select_cmd_with_child(self.target_child, sql)
        if not pdb_list:
            logger.info(f'target do not have pdb,{self._pid}')
            return
        for pdb in pdb_list:
            pdb_name = pdb.get("NAME")
            pdb_open_mode = pdb.get("OPEN_MODE")
            if pdb_open_mode == "READ WRITE":
                sql = f"alter pluggable database {pdb_name} close immediate;"
                return_code, std_out = exec_sql_cmd(self.target_child, sql, PexpectResult.CLOSE_PDB)
                if not return_code:
                    logger.error(f'alter pluggable error, return_code: {return_code}. sql: {sql}, stdout:{std_out}')
                    raise ErrCodeException(ErrorCode.ORACLE_EXEC_SQL_FAILED, [std_out],
                                           message=f'check compatibility error,pdb_name:{pdb_name}')
            sql = f"drop pluggable database {pdb_name} including datafiles;"
            return_code, std_out = exec_sql_cmd(self.target_child, sql, PexpectResult.DROP_PDB)
            if not return_code:
                logger.error(f'drop pluggable error, return_code: {return_code}. sql: {sql}, stdout:{std_out}')
                raise ErrCodeException(ErrorCode.ORACLE_EXEC_SQL_FAILED, [std_out],
                                       message=f'drop pluggable {pdb_name} error')

    @log_func_time_cost
    def check_single_pdb_pluggable(self, pdb_name):
        sql = (f"set serveroutput on;\n"
               f"DECLARE"
               f"  compatible BOOLEAN := FALSE;"
               f"BEGIN"
               f"  compatible := DBMS_PDB.CHECK_PLUG_COMPATIBILITY("
               f"  pdb_descr_file => '{self.get_pdb_meta_xml_path(pdb_name)}');"
               f"  if compatible then"
               f"    DBMS_OUTPUT.PUT_LINE('IS_PLUGGABLE_YES');"
               f"  else "
               f"    DBMS_OUTPUT.PUT_LINE('IS_PLUGGABLE_NO');"
               f"  end if;"
               f"END;\n"
               f"/")
        return_code, std_out = exec_sql_cmd(self.target_child, sql, PexpectResult.IS_PLUGGABLE_YES)
        if not return_code:
            logger.error(f'check compatibility error, return_code: {return_code}. sql: {sql}, stdout:{std_out}')
            raise ErrCodeException(ErrorCode.ORACLE_EXEC_SQL_FAILED, [std_out],
                                   message=f'check compatibility error,pdb_name:{pdb_name}')

    @log_func_time_cost
    def plug_single_pdb_into_target_ins(self, pdb_name):
        sql = (f"create pluggable database {pdb_name} using '{self.get_pdb_meta_xml_path(pdb_name)}' "
               f"copy file_name_convert=({self.build_file_name_convert_str(pdb_name)});")
        logger.info(f"sql:{sql}")
        return_code, std_out = exec_sql_cmd(self.target_child, sql, PexpectResult.CREATE_PDB)
        if not return_code:
            logger.error(f'create pluggable database error,return_code:{return_code}.sql:{sql},stdout:{std_out}')
            raise ErrCodeException(ErrorCode.ORACLE_EXEC_SQL_FAILED, [std_out],
                                   message=f'create pluggable {pdb_name} database error')

    def alter_single_pdb_open(self, pdb_name):
        sql = f"alter pluggable database {pdb_name} open;"
        return_code, std_out = exec_sql_cmd(self.target_child, sql, PexpectResult.PDB_OPENED)
        if not return_code:
            logger.error(
                f'alter pluggable database open error, return_code: {return_code}. sql: {sql}, stdout:{std_out}')
            raise ErrCodeException(ErrorCode.ORACLE_EXEC_SQL_FAILED, [std_out],
                                   message=f"alter pluggable database {pdb_name} open error")

    def build_file_name_convert_str(self, pdb_name):
        if self.target_omf_dest:
            return self.build_omf_build_file_name_convert_str(pdb_name)
        # 查询当前要插入的pdb有哪些dbf文件，拼接插入语句
        pdb_restore_info: PDBRestoreInfo = self.pdb_restore_map.get(pdb_name)
        logger.info(f'build_file_name_convert_str, pdb_restore_info: {pdb_restore_info}')

        def build_conversion_path(dbf_path):
            basename = os.path.basename(dbf_path)
            new_basename = f"{basename[:-4]}-{pdb_name}.dbf"
            full_path = os.path.join(self.restore_path, new_basename)
            return f"'{dbf_path}','{full_path}'"

        all_files = pdb_restore_info.datafile_list + pdb_restore_info.tempfile_list
        converts = [build_conversion_path(dbf) for dbf in all_files]
        line_sep = ",\r\n" if platform.system().lower() == "windows" else ",\n"
        return line_sep.join(converts)

    def build_omf_build_file_name_convert_str(self, pdb_name):
        # 查询当前要插入的pdb有哪些dbf文件，拼接插入语句
        converts = []
        pdb_restore_info: PDBRestoreInfo = self.pdb_restore_map.get(pdb_name)
        datafile, tempfile = self.get_recovery_path(self.target_omf_dest, self.target_db_name, pdb_name)
        for dbf in pdb_restore_info.datafile_list:
            basename = os.path.basename(dbf)
            full_path = os.path.join(datafile, basename)
            convert_path = f"'{dbf}','{full_path}'"
            converts.append(convert_path)
        for dbf in pdb_restore_info.tempfile_list:
            basename = os.path.basename(dbf)
            full_path = os.path.join(tempfile, basename)
            convert_path = f"'{dbf}','{full_path}'"
            converts.append(convert_path)
        # 拼装sql增加换行符，避免单行文本过长
        if platform.system().lower() == "windows":
            con_str = ",\r\n".join(converts)
        else:
            con_str = ",\n".join(converts)
        return con_str

    def handle_recover_path(self, pdb_name):
        omf_dest = get_omf_dest(self.target_ins_login_params)
        logger.info(f"omf_dest:{omf_dest}")
        self.target_omf_dest = omf_dest
        if not omf_dest:
            if not self.restore_path:
                self.restore_path = '+DATA'
                # 输入参数合法性
            while self.restore_path.endswith('/'):
                self.restore_path = self.restore_path[:-1]
            logger.info(f"omf is not enable,{self._pid}")
            return True
        datafile, tempfile = self.get_recovery_path(self.target_omf_dest, self.target_db_name, pdb_name)
        logger.info(f"datafile:{datafile},tempfile:{tempfile},target_db_name:{self.target_db_name}")
        if not omf_dest[0] == "+":
            if platform.system().lower() == Platform.WINDOWS:
                create_dir_recursive(datafile)
                create_dir_recursive(tempfile)
            else:
                exec_mkdir_cmd(datafile, is_check_white_list=False)
                exec_mkdir_cmd(tempfile, is_check_white_list=False)
                exec_lchown_dir_recursively(omf_dest, self.ora_db_user, self.oracle_group)
                logger.info(f"chown {omf_dest} {self.ora_db_user} {self.oracle_group} success")
            return True
        if platform.system().lower() == "windows":
            from oracle.common.windows_func import create_asm_dir
            ret, grid_home_path = get_grid_home_path()
            if not grid_home_path:
                logger.error(f"get grid oracle home error,{self._pid}")
                return False
            create_data_ret = create_asm_dir(datafile, self.asm_instance, grid_home_path)
            create_temp_ret = create_asm_dir(tempfile, self.asm_instance, grid_home_path)
            return create_data_ret and create_temp_ret
        else:
            from oracle.common.linux_common import create_asm_dir
            grid_env = get_asm_user_env_by_linux(self.asm_install_user)
            create_data_ret = create_asm_dir(datafile, grid_user=self.asm_install_user, grid_env=grid_env)
            create_temp_ret = create_asm_dir(tempfile, grid_user=self.asm_install_user, grid_env=grid_env)
            return create_data_ret and create_temp_ret

    def get_recovery_path(self, omf_dest, db_name, pdb_name):
        logger.info(f"self.pdb_restore_map:{self.pdb_restore_map},pdb_name:{pdb_name},db_name:{db_name}")
        pdb_restore_info: PDBRestoreInfo = self.pdb_restore_map.get(pdb_name)
        if omf_dest[0] == "+":
            datafile = str(os.path.join(omf_dest, db_name.upper(), pdb_restore_info.guid, 'DATAFILE'))
            tempfile = str(os.path.join(omf_dest, db_name.upper(), pdb_restore_info.guid, 'TEMPFILE'))
        else:
            datafile = str(os.path.join(omf_dest, db_name.upper(), pdb_restore_info.guid, 'datafile'))
            tempfile = str(os.path.join(omf_dest, db_name.upper(), pdb_restore_info.guid, 'tempfile'))
        logger.info(f"datafile:{datafile},tempfile:{tempfile}")
        return datafile, tempfile

    @log_func_time_cost
    def unplug_single_pdb(self, pdb_name):
        sql = f"alter pluggable database {pdb_name} close immediate;"
        return_code, std_out = exec_sql_cmd(self.assist_child, sql, PexpectResult.CLOSE_PDB_FORCE)
        if not return_code:
            logger.warning(f'alter pluggable close error, return_code: {return_code}. sql: {sql}, stdout:{std_out}')
        sql = f"alter pluggable database {pdb_name} unplug into '{self.get_pdb_meta_xml_path(pdb_name)}';"
        return_code, std_out = exec_sql_cmd(self.assist_child, sql, PexpectResult.UNPLUG_PDB)
        if not return_code:
            logger.error(f'alter pluggable unplug error, return_code: {return_code}. sql: {sql}, stdout:{std_out}')
            raise ErrCodeException(ErrorCode.ORACLE_EXEC_SQL_FAILED, [std_out],
                                   message=f'unplug pdb {pdb_name} into failed')
        sql = f"drop pluggable database {pdb_name} keep datafiles;"
        return_code, std_out = exec_sql_cmd(self.assist_child, sql, PexpectResult.DROP_PDB)
        if not return_code:
            logger.error(f'drop pluggable error, return_code: {return_code}. sql: {sql}, stdout:{std_out}')
            raise ErrCodeException(ErrorCode.ORACLE_EXEC_SQL_FAILED, [std_out],
                                   message=f'drop pdb {pdb_name} failed')

    def get_pdb_meta_xml_path(self, pdb_name):
        return os.path.join(self.main_backup_path, f"{pdb_name}.xml")
